常用的原生功能我相信各位讀者多少都看過
以下列舉幾個常見常用的。
我們時常在其他語言中會看到類似這樣的作法
const str = new String("Robin is handsome")
console.log(str.toString()); // "Robin is handsome"
看起來也非常合理,但是實際上在JS中可能有一些行為會與你想像的不同。
const str = new String("test yoyo")
typeof str // ?
各位猜看看這的型態是什麼?
答案是...
String
...嗎? 看起來非常合理!
但是實際上卻是...
Object
主要原因是他建立了一個字串包裹器的物件而不是單純建立 "test yoyo"
這個字串的基值。
你知道嗎...
typeof 是 "object" 的那些值內部額外會有一個特性
,叫做 [[Class]]
。
我知道你內心想的是什麼!
但是不是你想的那種 Class
,
你把它想成是一種內部的分類,
這個特性
很特別他無法被直接使用,
但是你可以使用 Object.prototype.toString(..)
方法來呼叫他。
Object.prototype.toString.call([6,6,6]); // "[object Array]"
Object.prototype.toString.call(/regex-literal/i); // "[object RegExp]"
而 undefined
和 null
也是
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
而 string
、number
和 boolean
並不是這樣,他們會由封裝用的包裹器
涉入。
接著我們就來講封裝用的包裹器
。
還記得之前說的各種型態的小精靈嗎?
如果基型值沒有特性和方法想要存取類似 .length
或是 .toString()
需要包裹基型值的物件包裹器,那我該怎麼做?
答案是...
什麼都不用做!
JS 會自動封裝基本型別的值,
該注意的是你不應該藉由直接使用物件形式試著預先最佳化
,
簡單來說就是你什麼都不該做,因為這樣做只會讓你的程式碼執行得更慢。
舉例來說,
不要做 new String("foo")
或是 new Number(18)
之類的動作,
直接優先使用 "foo"
或是 18
字面形式的基型值就可以了。
如果我就是要做怎麼辦?
也不是不行但是可能會遇到一些雷,
例如以下範例
const a = new Boolean(false);
if(!fasle) console.log("TEST); // 不會執行。
... 為什麼?
因為你建立的是一個"物件包裹器",物件本身是 object
,他又是 truthy
,所以產生的行為就會跟你原本建立的 false
,產生完全不一樣的結果。
封印解除!
如果有一個物件包裹器想要拿裡面的值該怎麼做?
可以直接使用 valueOf()
這個方法。
const a = new String("test");
const b = new String(123);
const c = new String(true);
a.valueOf(); // "test"
b.valueOf(); // 123
c.valueOf(); // true
解封裝也有隱含發生的可能,例如以下範例
const a = new String("test");
const b = a + "test" // 直接拿取 a 解封後的基型值
typeof a // "object"
typeof b // "string"
object
、 array
、 function
和正規表達式
這些值比較常看到被使用的方式就是直接用字面的方式如下範例
const a = [];
const b = {};
const c = function(){..};
const d = /^a.b*c/g;
接下來要分享如果使用建構器的方式可能會遇到的問題。
Array
建構器前面不用使用 new
,因為有加和沒加的結果是相同的。
const a = new Array(1,2,3);
const b = new Array(1,2,3);
const c = [1,2,3]
console.log(a); // [1,2,3]
console.log(b); // [1,2,3]
console.log(c); // [1,2,3]
當只有一個 number
參數帶入 Array
的建構器時,他不會把它當作陣列的內容,他會把他當一個長度來"預先設定陣列大小"。
可以觀察一下底下範例的差異。
const a = new Array(5);
const b = [ undefined, undefined, undefined, undefined, undefined];
const c = [];
c.length = 5;
console.log(a); // [ <5 empty items> ]
console.log(b); // [ undefined, undefined, undefined, undefined, undefined ]
console.log(c); // [ <5 empty items> ]
但是還記得我昨天在 值 這篇所論述的嗎?
JS 沒有預設大小這種事存在
也就等於是你有可能會插了空插槽,有可能會造成你創造出稀疏陣列,就是很莫名其妙的資料結構 xD
上面的範例是在 Chrome
所產生的,
然而在 Firefox
所顯示的卻不太一樣。
const a = new Array(5);
console.log(a); // [ <5 empty slots> ]
書上寫道原本更慘xD
原本在 FireFox
所顯示的會長這樣
const a = new Array(5);
console.log(a); // [ , , , , , ]
在最後放一個額外的
,
是因為在 ES5 中的串列的尾隨逗號是被允許的所以最後一個會被忽略。
雖然從上面的[ , , , , , ]
改為[ <5 empty slots> ]
是一個改進沒錯...
But 事情永遠都不是那麼如人所願...
剛剛看 a
和 b
的長的雖然不一樣但是顯示的行為相同,
但是他某些情況行為相同,但有時候又不同。
const a = new Array(5);
const b = [ undefined, undefined, undefined, undefined, undefined];
a.join('-');
b.join('-');
a.map(function(v,i){return i} ); // [ <5 empty items> ]
b.map(function(v,i){return i} ); // [ 0, 1, 2, 3, 4 ]
a.map
實際會失敗的原因簡單來說是那些插槽其實不是真的存在,就是他真的是"空插槽",不是真的插入 undefined
。
如果真的想要建置帶有實質的 undefined
值的陣列除了像我剛剛上面手動打之外你還可以使用 apply
const a = Array.apply(null, {length:5});
Anyway...
不要這樣做xDDD
這三種也盡量不要使用建構器,請也直接使用字面的形式。
我記得如果你在你的專案有使用 ESLint 他也會跟你說母湯安餒。
正規表示法直接選用的理由除了語法簡單外,還有因為效能問題,JS 引擎會在程式碼執行前預先編譯和快取。
但是如果說正規表達式有動態的需求就可以譬如以下範例
const name = 'robin';
const namePattern = new RegExp("\\b(?:" + name + ")+\\b", "ig");
const matches = text.match(namePattern);
這兩個的原生建構器比較常見,因為他們都沒有字面形式可以用 QQ
要建立日期物件值時可以用new Date()
,如果不使用new
拿到的值則會是字串形式的日期~
Error
這個建構器不管有沒有 new
行為都相同,通常會藉由 throw
運算子來使用這樣的錯誤物件如範例
function foo(){
if(!x){
throw new Error("x wasn't provided")
}
}
他跟 Date
和 Error
一樣沒有字面的形式,然後你不能使用 new
,因為他會直接噴錯xD
原生的建構器都自己的 .prototype
物件,以 String
來說
String#indexOf(..)
String#chartAt(..)
String#substr(..)
String#toUpperCase(..)
String#trim(..)
如果是一般使用直接使用的話會藉由原型委派
的機制讓他可以使用這些方法。
以上是我今天分享的內容
感覺論述的越來越抽象xDD
但是某些讀著讀著就是各種
哦哦哦哦哦哦!!!
感謝你的收看
我們明天見
你所不知道的 JS|導讀,型別與文法 (You Don't Know JS: Up & Going)